package main

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"os/exec"
	"sync"
	"time"

	"gitee.com/gricks/cron"
	jsoniter "github.com/json-iterator/go"
)

const (
	CronTaskMaxHistory = 10
)

var ErrExist = errors.New("exist")

type CronJobStatus int

const (
	CronJobStatus_Waiting CronJobStatus = iota
	CronJobStatus_Running
)

func (this CronJobStatus) String() string {
	status := [...]string{"waiting", "running"}
	return status[this]
}

type History struct {
	STime   time.Time
	ETime   time.Time
	Success bool
	Output  string
}

type CronJob struct {
	c *CronTask `json:"-"`

	Id        string
	Name      string
	Spec      string
	Desc      string
	CTime     time.Time
	Prev      time.Time
	PrevState bool
	Next      time.Time
	Script    string
	Status    CronJobStatus
	Histories []*History
}

func (this *CronJob) ID() string {
	return this.Id
}

func (this *CronJob) AddHistory(h *History) {
	this.c.mtx.Lock()
	defer this.c.mtx.Unlock()
	this.Histories = append(this.Histories, h)
	if len(this.Histories) > CronTaskMaxHistory {
		this.Histories = this.Histories[len(this.Histories)-CronTaskMaxHistory : len(this.Histories)]
	}
	this.Status = CronJobStatus_Waiting
}

func (this *CronJob) Run() {
	this.c.mtx.Lock()
	this.Status = CronJobStatus_Running
	this.c.mtx.Unlock()

	history := &History{}
	history.STime = time.Now().Truncate(time.Second)

	fname := fmt.Sprintf("script/%s_%d_%d.sh", this.ID(), rand.Int31(), history.STime.Unix())
	err := ioutil.WriteFile(fname, []byte(this.Script), 0644)
	if err != nil {
		history.ETime = time.Now().Truncate(time.Second)
		history.Success = false
		history.Output = fmt.Sprintf("Create Bash Err:%s", err)

		this.AddHistory(history)
		return
	}
	defer os.Remove(fname)

	var cout bytes.Buffer
	c := exec.Command("/bin/bash", fname)
	c.Stdout = &cout
	c.Stderr = &cout
	err = c.Run()
	history.ETime = time.Now().Truncate(time.Second)
	if err != nil {
		history.Success = false
	} else {
		history.Success = true
	}
	history.Output = string(cout.Bytes())
	this.AddHistory(history)
}

type CronTask struct {
	mtx   sync.Mutex `json:"-"`
	cron  *cron.Cron `json:"-"`
	Jobs  map[string]*CronJob
	Index int
}

func NewCronTask() *CronTask {
	return &CronTask{
		cron: cron.New(),
		Jobs: make(map[string]*CronJob),
	}
}

func (this *CronTask) Load(b []byte) error {
	if len(b) != 0 {
		err := jsoniter.Unmarshal(b, this)
		if err != nil {
			return err
		}
		for _, job := range this.Jobs {
			err := this.AddJob(job)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (this *CronTask) Start() {
	this.cron.Start()
}

func (this *CronTask) Stop() {
	this.cron.Stop()
}

func (this *CronTask) AddJob(job *CronJob) error {
	this.mtx.Lock()
	defer this.mtx.Unlock()
	if job.Id == "" {
		this.Index++
		job.Id = fmt.Sprintf("%d", this.Index)
	}
	if _, ok := this.Jobs[job.ID()]; ok {
		return ErrExist
	}
	job.c = this
	this.Jobs[job.ID()] = job
	return this.cron.AddJob(job.Spec, job)
}

func (this *CronTask) DelJob(id string) {
	this.mtx.Lock()
	defer this.mtx.Unlock()

	delete(this.Jobs, id)
	this.cron.DelJob(id)
}

func (this *CronTask) String() string {
	this.mtx.Lock()
	defer this.mtx.Unlock()

	b, _ := jsoniter.Marshal(this)
	return string(b)
}

func (this *CronTask) Stringify() string {
	this.mtx.Lock()
	defer this.mtx.Unlock()

	b, _ := jsoniter.MarshalIndent(this, "", "  ")
	return string(b)
}

func (this *CronTask) Snapshot() (*CronTask, error) {
	this.mtx.Lock()
	b, err := jsoniter.Marshal(this)
	if err != nil {
		return nil, err
	}
	this.mtx.Unlock()

	c := &CronTask{}
	jsoniter.Unmarshal(b, c)
	for _, e := range this.cron.Entries() {
		if job, ok := c.Jobs[e.Job.ID()]; ok {
			if len(job.Histories) > 0 {
				job.Prev = job.Histories[len(job.Histories)-1].STime
				job.PrevState = job.Histories[len(job.Histories)-1].Success
			} else {
				job.Prev = time.Time{}
				job.PrevState = true
			}
			job.Next = e.Next
		}
	}
	return c, nil
}
